--
-- Death knight rotation helper
--

-- '_G' stuff is to keep lualint happy

if _G['select'](2, _G['UnitClass']('player')) ~= 'DEATHKNIGHT' then return end

-------------------------------------------
-- Local references to globals for speed and lint's happiness
-------------------------------------------
local _G = _G

local UnitHealth = _G['UnitHealth']
local strsplit = _G['strsplit']
local max = _G['max']
local min = _G['min']
local GetTime = _G['GetTime']
local GetRuneCooldown = _G['GetRuneCooldown']
local GetRuneType = _G['GetRuneType']

local Rotor = _G['Rotor']

-- The module
local Mod = Rotor:NewModule('Death Knight')

-------------------------------------------
-- Constants
-------------------------------------------
local BLOOD_RUNE_TYPE   = 1
local UNHOLY_RUNE_TYPE  = 2
local FROST_RUNE_TYPE   = 3
local DEATH_RUNE_TYPE   = 4

local RUNE_COLORS = {
    {1, 0, 0},      -- Blood
    {0, 0.95, 0},   -- Unholy
    {0, 1, 1},      -- Frost
    {0.8, 0.1, 1}   -- Death
}

local SPEC_BLOOD = 1
local SPEC_FROST = 2
local SPEC_UNHOLY = 3

local MIN_POWER_FOR_FROST_STRIKE = 60
local MIN_POWER_FOR_RUNE_STRIKE = 90

--
-- Spell and aura defs
--
local spell_defs = {
    ob      = {id = 49020, rt = UNHOLY_RUNE_TYPE, rt2 = FROST_RUNE_TYPE}, -- Obliterate
    it      = {id = 45477, rt = FROST_RUNE_TYPE}, -- Icy touch
    ps      = {id = 45462, rt = UNHOLY_RUNE_TYPE}, -- Plague strike
    outb    = {id = 77575}, -- Outbreak
    how     = {id = 57330}, -- Horn of winter
    ds      = {id = 49998}, -- Death Strike
    hs      = {id = 55050}, -- Heart Strike
    rs      = {id = 56815}, -- Rune Strike
    dnd     = {id = 43265}, -- Death n Decay
    mf      = {id = 47528, icon_pos = 11}, -- mind freeze
}

local aura_defs = {
    how = {id = 57330, from_others = true}, -- Horn of Winter
    ff = {id = 59921, is_debuff = true}, -- Frost fever
    bp = {id = 55078, is_debuff = true}, -- Blood plague
}

local blood_spell_defs = {
    bs      = {id = 49222, icon_pos = 8}, -- Bone shield
    ibf     = {id = 48792, icon_pos = 10, with_buff = true}, -- icebound fortitude
    ams     = {id = 48707, icon_pos = 9, with_buff = true}, -- anti-magic shell
    bt      = {id = 48982, icon_pos = 7, must_exists = true},    -- blood tap
    lb      = {id = 49039, icon_pos = 6, icon_type = 'reminder', min_power = 40, must_exists = true},   -- lich borne
    drw     = {id = 49028, icon_pos = 5, icon_type = 'reminder', min_power = 60, must_exists = true},   -- dancing rune weapon
}
local blood_aura_defs = {
}
local frost_spell_defs = {
    hb      = {id = 49184, must_exists = true}, -- Howling blast
    fs      = {id = 49143}, -- Frost strike
}
local frost_aura_defs = {
    ff = {id = 59052}, -- Freezing fog
    km = {id = 51124}, -- Killing machine
}
local unholy_spell_defs = {
}
local unholy_aura_defs = {
}

-- Debuff ids

----------------------------------------
-- utilites
----------------------------------------

-- Local vars
local now = 0
local gcd = 0

local rune_counts = {0, 0, 0, 0} -- array of ready runes count, by types (with count of gcd)

local spells = {}
local buffs = {}
local debuffs = {}

--
-- Methods implementation
--
function Mod:Init(opts)
    Rotor.upper_text:SetFont('Fonts\\ArialN.ttf', 8, 'OUTLINE')

    for i = 1, 4 do
        rune_counts[i] = 0
    end

    spells, buffs, debuffs = Rotor:RegisterSpellsAndAuras(spell_defs, aura_defs)
    if Rotor.spec == SPEC_BLOOD then
        Rotor:RegisterSpellsAndAuras(blood_spell_defs, blood_aura_defs, spells, buffs, debuffs)
    elseif Rotor.spec == SPEC_FROST then
        Rotor:RegisterSpellsAndAuras(frost_spell_defs, frost_aura_defs, spells, buffs, debuffs)
    else
        Rotor:RegisterSpellsAndAuras(unholy_spell_defs, unholy_aura_defs, spells, buffs, debuffs)
    end

    Rotor:showUpperText()
    Rotor:showPowerDisplay()

    return true
end

function Mod:Done()
    spells = nil
    buffs = nil
    debuffs = nil
end

local function HaveOneRune(rt)
    return (rune_counts[rt] + rune_counts[DEATH_RUNE_TYPE]) > 0
end

local function HaveTwoRunes(rt1, rt2)
    return (min(rune_counts[rt1], 1) + min(rune_counts[rt2], 1) + rune_counts[DEATH_RUNE_TYPE]) >= 2
end

local function HaveTwoRunePairs(rt1, rt2)
    local drc = rune_counts[DEATH_RUNE_TYPE]
    local r1c = rune_counts[rt1]
    local r2c = rune_counts[rt2]
    local nrc

    drc = drc - (2 - r1c) - (2 - r2c)
    return (r1c + r2c + drc) >= 4
end

local function HaveB()
    return HaveOneRune(BLOOD_RUNE_TYPE)
end
local function HaveF()
    return HaveOneRune(FROST_RUNE_TYPE)
end
local function HaveU()
    return HaveOneRune(UNHOLY_RUNE_TYPE)
end
local function HaveUF()
    return HaveTwoRunes(UNHOLY_RUNE_TYPE, FROST_RUNE_TYPE)
end

local last_runes_text = ""
local function updateRunes()
    for i = 1, 4 do
        rune_counts[i] = 0
    end
    local text = ""
    for i = 1, 6 do
        local start, cooldown = GetRuneCooldown(i)
        local cdtime = math.max(math.ceil(start + cooldown - now), 0)
        local rt = GetRuneType(i)
        if (start < now) and (cdtime - gcd <= 0) then
            rune_counts[rt] = rune_counts[rt] + 1
        end
        local r, g, b = unpack(RUNE_COLORS[rt])
        if (start > now) or (cdtime >= 10) then
            cdtime = "X"
        elseif cdtime <= 0 then
            cdtime = "+"
        end
        text = text..string.format("|cff%02x%02x%02x%s|r", r * 255, g * 255, b * 255, cdtime)
    end

    if last_runes_text ~= text then
        Rotor:setUpperText(text)
        last_runes_text = text
    end
end

--[[
function DumpRunes()
    print('now:',GetTime())
    for i = 1, 6 do
        local start, cooldown = GetRuneCooldown(i)
        print(i, ': ', start, ' ', cooldown)
    end
end

function DumpRuneCounts()
    print('--------')
    for i = 1, 4 do
        print(i, ' = ', rune_counts[i])
    end
end
--]]

local function CheckUnsent(spell, time_since_last_sent)
    return spell and (spell.sentAt <= spell.doneAt) and (spell.doneAt + (time_since_last_sent or 0.5) < now)
end

local function CheckOffCdAndUnsent(spell, time_since_last_sent, wth)
    return spell and (spell.cd - (wth or 0) <= gcd) and CheckUnsent(spell, time_since_last_sent)
end

local function CheckOffCd(spell)
    return spell and (spell.cd <= gcd)
end

local function EnoughRunes(spell)
    if not spell.def.rt then
        return true
    elseif not spell.def.rt2 then
        return HaveOneRune(spell.def.rt)
    else
        return HaveTwoRunes(spell.def.rt, spell.def.rt2)
    end
end

function Mod:CastSent(spell)
end

local function CheckPlagues()
    if (debuffs.bp.time_left < 3) or (debuffs.ff.time_left < 3) then
        if CheckOffCdAndUnsent(spells.outb) then
            return spells.outb.id
        end

        if not CheckUnsent(spells.outb, 1) then
            -- wait one second for debuffs application
            return 0
        end

        if (debuffs.ff.time_left < 3) then
            local spell

            if Rotor.spec == SPEC_FROST then
                spell = spells.hb
            else
                spell = spells.it
            end
            if CheckUnsent(spell) and HaveF() then
                return spell.id
            end
        end

        if (debuffs.bp.time_left < 3) and CheckUnsent(spells.ps) and HaveU() then
            return spells.ps.id
        end
    end

    return 0
end

local function CheckHoW()
    return buffs.how.time_left < 3
    and CheckOffCdAndUnsent(spells.how)
end

function Mod:NextSkill(gcd_arg, cur_spell)
    gcd = gcd_arg + 0.1
    now = GetTime()

    updateRunes()

    if not Rotor.in_combat and CheckHoW() then
        return spells.how.id
    end

    -- Check plagues
    local spell_id = CheckPlagues()
    if spell_id ~= 0 then
        return spell_id
    end

    -- check horn
    if CheckHoW() then
        return spells.how.id
    end

    if Rotor.spec == SPEC_FROST then
        return self:NextFrostSkill(gcd_arg, cur_spell)
    elseif Rotor.spec == SPEC_BLOOD then
        return self:NextBloodSkill(gcd_arg, cur_spell)
    else
        return self:NextUnholySkill(gcd_arg, cur_spell)
    end
end

function Mod:NextFrostSkill(gcd_arg, cur_spell)
    -- high prio obli
    if CheckUnsent(spells.ob) and
        (HaveTwoRunePairs(FROST_RUNE_TYPE, UNHOLY_RUNE_TYPE)
            or ((buffs.km.time_left > gcd_arg) and HaveTwoRunes(FROST_RUNE_TYPE, UNHOLY_RUNE_TYPE))) then
        return spells.ob.id
    end

    -- power overcap
    if (Rotor.power >= 100) and CheckUnsent(spells.fs) then
        return spells.fs.id
    end

    -- rime
    if (buffs.ff.time_left > gcd) and CheckUnsent(spells.hb) then
        return spells.hb.id
    end

    -- normal obli
    if HaveTwoRunes(FROST_RUNE_TYPE, UNHOLY_RUNE_TYPE) and CheckUnsent(spells.ob) then
        return spells.ob.id
    end

    -- frost strike
    if Rotor.power >= 50 then
        return spells.fs.id
    end

    if CheckUnsent(spells.hb) and HaveF() then
        return spells.hb.id
    end

    if CheckOffCdAndUnsent(spells.how) then
        return spells.how.id
    end

    return 0
end

local function MinPowerForRuneStrike()
    if CheckOffCd(spells.drw) then
        return 60+30
    elseif CheckOffCd(spells.lb) then
        return 40+30
    else
        return 20+30
    end
end

function Mod:NextBloodSkill(gcd_arg, cur_spell)
    -- death strike
    if HaveUF() and CheckUnsent(spells.ds) then
        return spells.ds.id
    end

    -- rune strike
    if (Rotor.power >= MinPowerForRuneStrike()) then
        return spells.rs.id
    end

    -- hearth strike
    if rune_counts[BLOOD_RUNE_TYPE] > 1 and CheckUnsent(spells.hs) then
        return spells.hs.id
    end

    if CheckOffCdAndUnsent(spells.how) then
        return spells.how.id
    end

    return 0
end

function Mod:NextUnholySkill(gcd_arg, cur_spell)
    -- todo
    return 0
end

-- Command line handler
function Mod:HandleCommandLine(cmd)
    local cmd, args = strsplit(' ', (cmd or ''):lower(), 2)
    -- handle command line here
end

-- Module detection
function Mod:Detect()
    return Rotor.spec == SPEC_FROST or Rotor.spec == SPEC_BLOOD
end
